/**
 * @file
 * X11 library interface implementation.
 *
 * Kisa provides spell checking as you type and displays the result in a small
 * window on your Desktop.
 *
 * Copyright (c) 2009 by Pete Black <theblackpeter@gmail.com>
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 2 of the License, or (at your option) any later
 * version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
 * Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * @author Pete Black <theblackpeter@gmail.com>
 */
#include <QApplication>
#include <QRegExp>
#include <QStringList>
#include <QtDebug>
#include <X11/XKBlib.h>
#include <X11/Xatom.h>
#include "x11lib.h"

/**
 * Instances of this class will have this name.
 * @note this is not a unique identifier, just a common name.
 */
#ifndef OBJECT_NAME
#define OBJECT_NAME "x11lib"
#endif

/**
 * Name used when outputting debug information.
 */
#ifndef OBJECT_DEBUG_NAME
#define OBJECT_DEBUG_NAME "x11lib:"
#endif

/**
 * Default Xlib key buffer size. Utf-8 is variable-length character encoding
 * that uses 4 bytes at the most. Remember we are only interested in the
 * character, so null-termination ('\0') is unimportant (Qt takes care of that).
 */
#ifndef KEY_SIZE
#define KEY_SIZE 4
#endif


// define and initialize static variables
XErrorHandler X11Lib::m_oldXErrorHandler = (XErrorHandler) 0;

X11Lib::X11Lib(const QString& displayString) {
  initDisplay(displayString);
  initMembers();

  qDebug() << OBJECT_DEBUG_NAME << OBJECT_NAME << "created";
}

X11Lib::~X11Lib() {
  // revert back to the old X11 error handler
  (void) XSetErrorHandler(m_oldXErrorHandler);

  qDebug() << OBJECT_DEBUG_NAME << "closing connection to X server";
  XCloseDisplay(m_display);

  qDebug() << OBJECT_DEBUG_NAME << OBJECT_NAME << "destroyed";
}

void X11Lib::addNotification(const Window& window, KisaEvent::Type type) {
  EventMask eventMask;

  switch(type) {
    case KisaEvent::None:
      eventMask = NoEventMask;
      break;

      // just a keypress event
    case KisaEvent::KeyPress:
      eventMask = KeyPressMask;
      break;

      // a VisibilityNotify
    case KisaEvent::WindowStateChange:
      eventMask = VisibilityChangeMask;
      break;

      // CreateNotify, MapNotify events, a window was either
      // created, made visible or re-configured
    case KisaEvent::WindowActivate:
      eventMask = KeyPressMask | SubstructureNotifyMask;
      break;

    default:
      ;
  }

  // NOTE: add other mask here such as KeymapStateMask

  // careful not to call this function too often, ignore the root window
  if(window != m_rootWindow)
    changeDeepNotification(window, eventMask);
}

void X11Lib::changeDeepNotification(const Window& window,
                                     const EventMask& eventMask) {
  // make sure we have don't any crappy windows
  if(!isWindowDecent(window))
    return;

  Window dummyWindow; // root window, dummy return, not used
  Window parentWindow; // not used either
  Window* childWindows;
  unsigned int totalChildWindows = 0;
  XQueryTree(m_display, window, &dummyWindow, &parentWindow, &childWindows,
             &totalChildWindows);

  // update the event subscription
  changeNotification(window, eventMask);

  // now get all the children of the window
  for(unsigned int i = 0; i < totalChildWindows; i++) {
    // recursive step
    changeDeepNotification(childWindows[i], eventMask);
  }

  if(!childWindows)
    XFree((char*) childWindows);
}

void X11Lib::changeNotification(const Window& window, EventMask eventMask) {
  // make sure we have don't have a crappy window
  if(!isWindowDecent(window))
    return;

  // if there is no event mask, remove any notification right away
  if(eventMask == NoEventMask) {
    removeNotification(window);
    return;
  }

  // skip our own window for key and create events but not notification of
  // window visibility change
  if(isOurOwnWindow(window))
    eventMask = VisibilityChangeMask;

  // OK then, every thing is as it should be, change the notification...
  XSelectInput(m_display, window, eventMask);
  XFlush(m_display);

  // increment the total windows we have subscription to
  m_totalWindowSubscriptions++;

#ifdef FULL_DEBUG
  printNotificationOutput(window, eventMask);
#endif
}

const QString X11Lib::getKeyboardLayoutMap() {
  if(!m_keyboardLayouts.isEmpty())
    return m_keyboardLayouts[m_keyboardLayoutIndex];

  return QString();
}

const QString X11Lib::getWindowName(const Window& window) {
  XTextProperty textProperty;
  QString name;
  if(XGetWMName(m_display, window, &textProperty))
    name = QString::fromUtf8((const char*) textProperty.value);

  return name;
}

// TODO: remove when KeyboardLayoutChange events start working again
int X11Lib::getXKbKeyboardLayoutIndex() {
  XkbStateRec state;

  // TODO: check the return value, which is almost always zero
  // http://tronche.com/gui/x/xlib/introduction/errors.html#Status
  XkbGetState(m_display, XkbUseCoreKbd, &state);

  return state.group;
}

// TODO: remove when KeyboardLayoutChange events start working again
QStringList X11Lib::getXKbKeyboardLayouts() {
  XkbDescPtr keyboard = XkbGetKeyboard(m_display,
                                       XkbAllComponentsMask,
                                       XkbUseCoreKbd);

  // symbols are on the form of, check with setxkbmap -print
  // pc_us_se_2_il_3_nec_vndr/jp_4_inet(evdev)_group(alt_shift_toggle)
  QString symbols = XGetAtomName(m_display, keyboard->names->symbols);

  int first = symbols.indexOf("_");

  // remove the first pc_
  symbols = symbols.remove(0, first + 1);

  first = symbols.indexOf("_");

  // add the first layout to the list
  QStringList keyboardLayouts;
  QString layout = symbols.left(first);
  keyboardLayouts << layout;

  // and remove it and the subsequent "_"
  symbols = symbols.remove(0, first + 1);

  for(int i = 1; i <= XkbNumKbdGroups; i++) {
    QString nextNumber = QString::number(i + 1);
    if(!symbols.contains(nextNumber))
        break;

    int nextNumberIndex = symbols.indexOf(nextNumber);

    // get the layout minus the "_" and number
    layout =  symbols.left(nextNumberIndex - 1);

    keyboardLayouts << layout;
    // and remove it and the subsequent "_", number and "_"
    symbols = symbols.remove(0, nextNumberIndex + 2);
  }

  XkbFreeKeyboard(keyboard, XkbAllComponentsMask, true);
  return keyboardLayouts;
}

void X11Lib::initDisplay(QString displayString) {
  if(displayString.isEmpty()) {
    // try the system display from the environment variable $DISPLAY
    displayString = getenv("DISPLAY");

    if(displayString.isEmpty())
      // use system default display on :0
      displayString = ":0";
  }

  m_display = XOpenDisplay(displayString.toAscii());

  if(!m_display) {
    QString message = OBJECT_NAME;
    message += " could not connect to X server on " + displayString;
    qFatal("%s", message.toLocal8Bit().constData());
  }
  else
    qDebug() << OBJECT_DEBUG_NAME << "connected to X server on display" << displayString;

  m_rootWindow = XDefaultRootWindow(m_display);

  if(!m_rootWindow) {
    QString message = OBJECT_NAME;
    message += " could not get root window on display " + displayString;
    qFatal("%s", message.toLocal8Bit().constData());
  }
  else
    qDebug() << OBJECT_DEBUG_NAME << "root window is" << showbase << hex << m_rootWindow;
}

void X11Lib::initMembers() {
  // instance variables
  m_totalWindowSubscriptions = 0;

  // the list of X event types that we use
  m_eventTypes[0] = "None";
  m_eventTypes[2] = "KeyPress";
  m_eventTypes[15] = "VisibilityNotify";
  m_eventTypes[16] = "CreateNotify";

  // and the list of all the possible X visibility states
  m_visibilityStates[0] = "VisibilityUnobscured";
  m_visibilityStates[1] = "VisibilityPartiallyObscured";
  m_visibilityStates[2] = "VisibilityFullyObscured";

  // override default X11 error handler
  m_oldXErrorHandler = XSetErrorHandler(setErrorHandler);

  // load the available keyboard layouts
  m_keyboardLayouts = getXKbKeyboardLayouts();
  m_keyboardLayoutIndex = getXKbKeyboardLayoutIndex();
}

bool X11Lib::isOurOwnWindow(const Window& window) {
  // compare returns 0 on match
  QString windowName = getWindowName(window);
  return (!windowName.isEmpty() &&
          !windowName.compare(QApplication::applicationName(),
                              Qt::CaseInsensitive));
}

bool X11Lib::isWindowDecent(const Window& window) {
  // just in case some idiot sends the wrong window id, like a default 0 instead
  if(window < m_rootWindow) {
#ifdef FULL_DEBUG
    qDebug() << OBJECT_DEBUG_NAME << "skipping window" << "id:" << showbase << hex << window << "(not a window)";
#endif
    return false;
  }

  // skip unmapped and unviewable windows
  if(!isWindowVisible(window)) {
#ifdef FULL_DEBUG
    qDebug() << OBJECT_DEBUG_NAME << "skipping window" << "id:"<< showbase << hex << window << "(window not visible)";
#endif
    return false;
  }

  QString windowName = getWindowName(window);

  // skip windows from the ignore listen
  if(windowName.contains(QRegExp(m_ignoreList, Qt::CaseInsensitive))) {
#ifdef FULL_DEBUG
    qDebug() << OBJECT_DEBUG_NAME << "skipping window" << windowName << "id:" << showbase << hex << window << "(regexp match)";
#endif
    return false;
  }

  // ignore windows with override_redirect attributes (i.e. a popup)
  if(isWindowPopup(window)) {
#ifdef FULL_DEBUG
    qDebug() << OBJECT_DEBUG_NAME << "skipping window" << windowName << "id:" << showbase << hex << window << "(pop-up)";
#endif
    return false;
  }

  return true;
}

bool X11Lib::isWindowPopup(const Window& window) {
  // NOTE: could use save_under as well
  XWindowAttributes windowAttributes;
    if(!XGetWindowAttributes(m_display, window, &windowAttributes))
      return true;

  return windowAttributes.override_redirect;
}

bool X11Lib::isWindowVisible(const Window& window) {
  // get the window attributes, if the window has none this will generate a BadWindow error
  XWindowAttributes windowAttributes;
  if(!XGetWindowAttributes(m_display, window, &windowAttributes))
    return false;

  // special case for our own application window that for some reason stays
  // unmapped, so ignore that
  if(isOurOwnWindow(window))
    return true;

  // a window is not visible if it is unmapped and unviewable
  if(windowAttributes.map_state == IsUnmapped || windowAttributes.map_state
      == IsUnviewable)
    return false;

  return true;
}

QChar X11Lib::lookupKey(XKeyEvent& xKey) {
  char keyBuffer[KEY_SIZE];

  // XLookupString() returns the number of characters that are stored in the
  // buffer. It should be utf-8 safe, this may change though.
  int nChars = XLookupString(&xKey, keyBuffer, KEY_SIZE, NULL, NULL);

  // modifiers (shift, alt, ctrl...)  and arrow keys will result in nChars = 0
  if(nChars > 0 && keyBuffer != NULL)
    return QString::fromUtf8(keyBuffer, nChars).at(0);

  return QChar();
}

KisaEvent* X11Lib::nextEvent() {
  // get the event from the X server event queue
  XEvent xEvent;
  XNextEvent(m_display, &xEvent);

  switch(xEvent.type) {
    case XKeyPress: {
      Window window = xEvent.xkey.window;

      // get the key string from the XEvent which containing one unicode
      // (utf-8) character
      const QChar key = lookupKey(xEvent.xkey);

      // TODO: remove when KeyboardLayoutChange events start working again
      m_keyboardLayoutIndex = getXKbKeyboardLayoutIndex();

      if(m_keyboardLayoutIndex < 0 || m_keyboardLayoutIndex > XkbMaxKbdGroup)
        m_keyboardLayoutIndex = 0;

      QString layout = QString();
      layout = m_keyboardLayouts[m_keyboardLayoutIndex];

#ifdef FULL_DEBUG
      if (key.isNull())
      qDebug() << OBJECT_DEBUG_NAME << "new KeyPress event, keycode:" << showbase << hex << xEvent.xkey.keycode << "key sym:" << XKeycodeToKeysym(m_display, xEvent.xkey.keycode, 0) << "from id:" << window;

      else
      qDebug() << OBJECT_DEBUG_NAME << "new KeyPress event, key:" << key << "layout" << layout << "keycode:" << showbase << hex << xEvent.xkey.keycode << "key sym:" << XKeycodeToKeysym(m_display, xEvent.xkey.keycode, 0) << "from id:" << window;
#endif

      return new KisaEvent(KisaEvent::KeyPress, key, layout, window);
    }

      // visibility changed for out application
    case VisibilityNotify: {
      Window window = xEvent.xvisibility.window;

      int state = xEvent.xvisibility.state;
#ifdef FULL_DEBUG
      qDebug() << OBJECT_DEBUG_NAME << "new VisibilityNotify event" << m_visibilityStates[state] << "from id:" << showbase << hex << window;
#endif
      return new KisaEvent(KisaEvent::WindowStateChange, state, window);
    }

      // a window was made "visible"
    case MapNotify: {
      Window window = xEvent.xmap.window;
#ifdef FULL_DEBUG
      qDebug() << OBJECT_DEBUG_NAME << "new MapNotify event from from id:" << showbase << hex << window;
#endif
      return new KisaEvent(KisaEvent::WindowActivate, window);
    }

      // a new window was created, note that all windows are not really useful
      // as they are not always mapped
      // this event gets thrown around quite a bit, but we'll include it anyway
    case CreateNotify: {
      Window window = xEvent.xcreatewindow.window;
#ifdef FULL_DEBUG
      qDebug() << OBJECT_DEBUG_NAME << "new CreateNotify event from id:" << showbase << hex << window;
#endif
      return new KisaEvent(KisaEvent::WindowActivate, window);
    }

      // keep this around in case it starts working, not very useful for now
      // see http://tronche.com/gui/x/xlib/events/key-map.html
#ifdef FULL_DEBUG
    case KeymapNotify: {
      Window window = xEvent.xcreatewindow.window;
      qDebug() << OBJECT_DEBUG_NAME << "new KeymapNotify event from id:" << showbase << hex << window;
      return new KisaEvent();
    }
#endif

    // this event usually occurs when the keyboard mapping changes, such as on
    // a layout change, much better then KeymapNotify above, but only useful if
    // we're using something other then kxkb, such as xbd itself
#ifdef FULL_DEBUG
    case MappingNotify: {
      Window window = xEvent.xcreatewindow.window;
      qDebug() << OBJECT_DEBUG_NAME << "new MappingNotify event from id:" << showbase << hex << window;
      return new KisaEvent();
    }
#endif

    default:
#ifdef FULL_DEBUG
      qDebug() << OBJECT_DEBUG_NAME << "new" << xEvent.type << "event";
#endif
      return new KisaEvent();
  }

  return new KisaEvent();
}

void X11Lib::printNotificationOutput(const Window& window,
                                      const EventMask& eventMask) {
  QString windowName = getWindowName(window);

  if(eventMask == NoEventMask)
    qDebug() << OBJECT_DEBUG_NAME << "removed notification for" << windowName << "id:" << showbase << hex << window;

  else if(eventMask == (KeyPressMask | SubstructureNotifyMask))
    qDebug() << OBJECT_DEBUG_NAME << "added KeyPress and CreateNotify notification for" << windowName << "id:" << showbase << hex << window;

  else if(eventMask == KeyPressMask)
    qDebug() << OBJECT_DEBUG_NAME << "added KeyPress notification for" << windowName << "id:" << showbase << hex << window;

  else if(eventMask == SubstructureNotifyMask)
    qDebug() << OBJECT_DEBUG_NAME << "added CreateNotify notification for" << windowName << "id:" << showbase << hex << window;

  else if(eventMask == VisibilityChangeMask)
    qDebug() << OBJECT_DEBUG_NAME << "added VisibilityNotify notification for" << windowName << "id:" << showbase << hex << window;

  else
    qDebug() << OBJECT_DEBUG_NAME << "added notification for" << windowName << "id:" << showbase << hex << window;
}

void X11Lib::reAddNotifications() {
  // use the default event masks, KeyPressMask and SubstructureNotifyMask
  changeDeepNotification(m_rootWindow);
}

void X11Lib::removeAllNotifications() {
  changeDeepNotification(m_rootWindow, NoEventMask);

  m_totalWindowSubscriptions = 0;
}

void X11Lib::removeNotification(const Window& window) {
  XSelectInput(m_display, window, NoEventMask);
  XFlush(m_display);

  m_totalWindowSubscriptions--;

#ifdef FULL_DEBUG
  printNotificationOutput(window, NoEventMask);
#endif
}

void X11Lib::replaceInClient(const Window& window,
                             const QString& misspelled,
                             const QString& suggestion,
                             const QString& residual) {
  // create the event struct
  XKeyEvent xKey;
  xKey.type = XKeyPress; // of event
  xKey.display = m_display; // Display the event was read from
  xKey.window = window; // "event" window it is reported relative to
  xKey.root = m_rootWindow; // root window that the event occurred on
  xKey.time = CurrentTime; // in ms
  xKey.state = XNone; // modifier state
  xKey.x = xKey.y = 0; // pointer x, y coordinates in event window
  xKey.x_root = xKey.y_root = 0; // coordinates relative to root
  xKey.same_screen = true;

  bool propagate = false; // send to every client, is supposed to be false

  // set the keycode to the backspace key
  xKey.keycode = XKeysymToKeycode(xKey.display, XK_BackSpace);

  // erase the misspelled word in the client application window by sending back
  // equal amount of backspace keys as characters in the word plus the residual,
  // example word + residual = "ok  " -> send back 4 backspaces
  int characters = misspelled.length() + residual.length();
  for(int i = 0; i < characters; i++) {
    XSendEvent(m_display, window, propagate, KeyPressMask, (XEvent*) &xKey);
    XFlush(m_display);
  }

  // now send the characters of the new word + the residual
  sendString(window, suggestion + residual);
}

void X11Lib::sendBogusXEvent() {
  // create the event struct
  XMapEvent xMapEvent;
  xMapEvent.type = MapNotify;
  xMapEvent.display = m_display;
  xMapEvent.window = m_rootWindow;
  xMapEvent.override_redirect = true;

  qDebug() << OBJECT_DEBUG_NAME << "sending bogus XEvent...";

  XSendEvent(xMapEvent.display, xMapEvent.window, xMapEvent.override_redirect,
             SubstructureNotifyMask, (XEvent*) &xMapEvent);
  XFlush(m_display);
}


void X11Lib::sendString(const Window& window, const QString& text) {
  // create the event struct
  XKeyEvent xKey;
  xKey.type = XKeyPress; // of event
  xKey.display = m_display; // Display the event was read from
  xKey.window = window; // "event" window it is reported relative to
  xKey.root = m_rootWindow; // root window that the event occurred on
  xKey.time = CurrentTime; // in ms
  xKey.state = XNone; // modifier state
  xKey.x = xKey.y = 0; // pointer x, y coordinates in event window
  xKey.x_root = xKey.y_root = 0; // coordinates relative to root
  xKey.same_screen = true;

  bool propagate = false; // send to every client, is supposed to be false

  // now send the characters of the string
  for(int i = 0; i < text.length(); i++) {
    // get the single character and its string representation
    const QChar keyChar = text.at(i);

    // set the modifier mask, that helps us get the character's right case
    if(keyChar.isUpper())
      xKey.state = ShiftMask;
    else
      xKey.state = XNone;

    // last, get the keycode that we need for sending the key event, using the
    // Keysyms from X11/keysymdef.h
    // the mechanisms by which Xlib obtains them is implementation-dependent, as
    // there is there is no 1-to-1 mapping between keysyms and keycodes, however
    // Qt helps out here as we can just use the raw utf-8 value of the character
    KeySym keySym = keyChar.unicode();
    xKey.keycode = XKeysymToKeycode(xKey.display, keySym);

    XSendEvent(m_display, window, propagate, KeyPressMask, (XEvent*) &xKey);
    XFlush(m_display);

#ifdef FULL_DEBUG
    qDebug() << OBJECT_DEBUG_NAME << "sent key:" << keyChar << "key code:" << showbase << hex << xKey.keycode << "key sym:" << keySym << "back to client on id:" << xKey.window;
#endif
  }

  qDebug() << OBJECT_DEBUG_NAME << "sent back" << text << "to client window id:" << showbase << hex << xKey.window;
}

int X11Lib::setErrorHandler(Display* d, XErrorEvent* xe) {
  // create a return buffer for the error message, 256 bytes should do
  const int BUFFER_SIZE = 256;
  char errorMessage[BUFFER_SIZE];

  int errorCode = xe->error_code;
  int requestCode = xe->request_code;

  // get the message
  XGetErrorText(d, errorCode, errorMessage, BUFFER_SIZE);

  // don't us a qWarning here as we can generate a lot of BadWindow errors
  qDebug() << OBJECT_DEBUG_NAME << "X error on display:" << XDisplayString(d) << errorMessage << requestCode;

  return 0;
}

// TODO: remove when KeyboardLayoutChange events start working again
void X11Lib::updateKeyboardLayouts() {
  m_keyboardLayouts = getXKbKeyboardLayouts();
}
